package de.dnb.basics.statistics;

import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;

import de.dnb.basics.applicationComponents.NullIterator;
import de.dnb.basics.applicationComponents.StringUtils;
import de.dnb.basics.cloneable.CopyObjectUtils;

/**
 * A collection similar to a Map, but which may associate multiple 
 * values with a single key. If you call add(K, V) twice, with the same key 
 * but different values, the multimap contains mappings from the key to 
 * both values.
 * 
 * Depending on the implementation, a multimap may or may not allow duplicate 
 * key-value pairs. In other words, the multimap contents after adding the same 
 * key and value twice varies between implementations. In multimaps allowing 
 * duplicates, the multimap will contain two mappings, and get will return 
 * a collection that includes the value twice. In multimaps not supporting 
 * duplicates, the multimap will contain a single mapping from the key to the 
 * value, and get will return a collection that includes the value once. 
 * 
 * @author Christian_2
 *
 * @param <K>
 * @param <V>
 */
public abstract class Multimap<K, V> implements Iterable<K>, Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 9199230459419376721L;

	/**
	 * Nicht final wegen Serialisierung.
	 */
	protected Map<K, Collection<V>> map;

	protected Multimap(Map<K, Collection<V>> values) {
		super();
		this.map = values;
	}

	/**
	 * Parameterlos wegen Serialisierung.
	 */
	private Multimap() {
	}

	/**
	 * Muss in einer Unterklasse überschrieben werden.
	 * 
	 * @return	neue Collection der Werte
	 */
	protected abstract Collection<V> getNewValueCollection();

	/**
	 * Füge weiteren Wert zum Schlüssel hinzu.
	 * 
	 * @param index		kann je nach Implementierung auch null sein
	 * @param value		kann je nach Implementierung auch null sein
	 */
	public final void add(final K index, final V value) {
		Collection<V> vColl = map.get(index);
		if (vColl == null) {
			vColl = getNewValueCollection();
			map.put(index, vColl);
		}
		vColl.add(value);
	}

	/**
	 * Fügt die Werte der Multimap hinzu. Ist die Wertemenge leer:
	 * keine Wirkung.
	 * 
	 * @param index		kann je nach Implementierung auch null sein	
	 * @param values	nicht null
	 */
	public final void addAll(final K index, final Iterable<V> values) {
		Objects.requireNonNull(values);
		for (V v : values) {
			add(index, v);
		}
	}

	/**
	 * Erzeugt einen leeren Eintrag für den Schlüssel key, wenn die zugehörigen 
	 * Werte noch unbekannt sind. Sonst keine Aktion.
	 * 
	 * @param key	Schlüssel, kann je nach Implementierung auch null sein
	 */
	public final void add(final K key) {
		Collection<V> vColl = map.get(key);
		if (vColl == null) {
			vColl = getNewValueCollection();
			map.put(key, vColl);
		}
	}

	/**
	 * 
	 * @param key	beliebig
	 * @return		key in Multimap enthalten. Das ist auch dann der Fall,
	 * 				wenn über Aufruf von {@link #add(Object)} eine
	 * 				leere Value-Collection erzeugt wurde
	 */
	public final boolean containsKey(final K key) {
		return map.containsKey(key);
	}

	public boolean containsValue(Object value) {
		for (Collection<V> collection : map.values()) {
			if (collection.contains(value)) {
				return true;
			}
		}
		return false;
	}

	public boolean containsEntry(Object key, Object value) {
		Collection<V> collection = map.get(key);
		return collection != null && collection.contains(value);
	}

	/**
	 * Zurücksetzen.
	 */
	public final void clear() {
		map.clear();
	}

	/**
	 * Iterator über Schlüssel.
	 * 
	 * @return	nicht null
	 */
	public final Iterator<K> keysIterator() {
		return map.keySet().iterator();
	}

	@Override
	public final Iterator<K> iterator() {
		return keysIterator();
	}

	/**
	 * 
	 * Gibt eine neue Collection der Werte zu key. Diese kann verändert
	 * werden. Vorsicht ist geboten, wenn der Zustand Werte selbst 
	 * verändert wird.
	 * 
	 * @param key	Schlüssel
	 * @return		Die Werte, die zum Schlüssel gehören (das kann auch eine
	 * 				leere Collection sein) oder null, wenn kein
	 * 				Mapping vorliegt
	 */
	public final Collection<V> get(final K key) {
		Collection<V> collectionForKey = map.get(key);
		if (collectionForKey == null)
			return null;
		else {
			Collection<V> valueCollection = getNewValueCollection();
			valueCollection.addAll(collectionForKey);
			return valueCollection;
		}

	}

	/**
	 * 
	 * @param key	Schlüssel
	 * @return		Eine neue Collection der Werte zu key, nicht null, 
	 * 				eventuell leer
	 */
	public final Collection<V> getNullSafe(final K key) {
		Collection<V> kValues = get(key);
		if (kValues != null)
			return kValues;
		else
			return new LinkedList<>();
	}

	/**
	 * Gibt eine neue Collection vom Typ, der von 
	 * {@link #getNewValueCollection()} geliefert wird, und die alle
	 * Werte enthält.
	 * 
	 * @return	nicht null. Eventuell leer, je nach Typ der Value-Collection
	 * 			kann auch jeder value einmal oder mahrmals auftreten.
	 */
	public final Collection<V> flatten() {
		Collection<V> valueCollection = getNewValueCollection();
		for (Iterator<K> iterator = keysIterator(); iterator.hasNext();) {
			K key = iterator.next();
			valueCollection.addAll(get(key));
		}
		return valueCollection;
	}

	/**
	 * 
	 * @return 	Iterator über alle Werte. Kommt ein Wert in mehreren
	 * 			Collections vor, so wird er auch bei Iteration
	 * 			mehrfach erfasst.
	 */
	public final Iterator<V> valuesIterator() {
		return new ValuesIterator();
	}

	private class ValuesIterator implements Iterator<V> {

		final Iterator<K> keysIterator = keysIterator();

		K actualKey;

		Collection<V> actualCollection;

		Iterator<V> actualValueIterator = new NullIterator<V>();

		@Override
		public boolean hasNext() {
			setPointerToNextNonemptyCollectionIfNecessary();
			return actualValueIterator.hasNext();
		}

		@Override
		public V next() {
			setPointerToNextNonemptyCollectionIfNecessary();
			return actualValueIterator.next();
		}

		/**
		 * Für den Fall, dass der Zeiger auf das Ende einer Collection
		 * zeigt:
		 * 	setzt den Zeiger auf den Beginn der nächsten nichtleeren 
		 * 	Collection, wenn es noch eine gibt.
		 */
		private void setPointerToNextNonemptyCollectionIfNecessary() {
			// nicht nötig?
			if (actualValueIterator.hasNext())
				return;
			getNextNonemptyCollectionIfExists();
		}

		/**
		 * 
		 */
		private void getNextNonemptyCollectionIfExists() {
			while (keysIterator.hasNext()) {
				actualKey = keysIterator.next();
				// nicht get() verwenden, da neue Collection!
				actualCollection = map.get(actualKey);
				actualValueIterator = actualCollection.iterator();
				if (!actualCollection.isEmpty()) {
					return;
				}
			}
		}

		@Override
		public void remove() {
			actualValueIterator.remove();
		}

	}

	/**
	 * Anzahl der Schlüssel.
	 * 
	 * @return	Zahl
	 */
	public int getKeyCount() {
		return map.size();
	}

	@Override
	public String toString() {
		String s = "";
		for (Iterator<K> iterator = keysIterator(); iterator.hasNext();) {
			K k = iterator.next();
			Collection<V> kValues = get(k);
			String listAsString = StringUtils.concatenate("\t", kValues);
			s += k + "\t" + listAsString;
			if (iterator.hasNext())
				s += "\n";
		}
		return s;
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Multimap<String, String> multimap =
			new TreeMultimap<>(String.CASE_INSENSITIVE_ORDER,
				String.CASE_INSENSITIVE_ORDER);
		multimap.add("D");
		multimap.add("b");
		multimap.add("c");
		multimap.add("e", "1");
		multimap.add("A", "x");

		//		multimap.add("A", "Y");
		//		multimap.add("A", "z");
		//		multimap.add("A", "X");
		//
		//		multimap.add("b", "1");
		//		multimap.add("b", "2");
		//
		//		multimap.add("C", "1");
		//
		//		multimap.add("A");

		System.out.println(multimap);
		System.out.println();

		for (Iterator<String> iterator = multimap.valuesIterator(); iterator
			.hasNext();) {
			System.out.println(iterator.next());
			iterator.remove();

		}
		System.out.println();
		System.out.println(multimap);

	}

}